Skip to content

fix(host): refuse a workflow id that is not a file name on the read path (#2609) - #2610

Merged
Birfy merged 1 commit into
mainfrom
dev/2609-workflow-store-id-guard
Nov 7, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/2609-workflow-store-id-guard

Conversation

@Birfy

@Birfy Birfy commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev
Fixes #2609
Owner: Dev
Reviewer: Reviewer

What changed

core/src/host/workflow-store.js — readDefinition now asks the same question save has always asked: if (!isWorkflowId(id)) return null. The read joins the caller's id onto workflows/, so an id such as ../outside/stolen used to read a JSON file written outside the workspace's workflows/ folder and serve it as this workspace's own definition. Every read entry point goes through this one function — open, preview, bytesOf, customize, setDefault, and the stored default key list() feeds back in — so the guard is here once and on no other entry point. An illegal id answers the existing workflow/not-found (no new code); legal ids and the two preset ids behave exactly as before.

Nothing else in the store changes: the folder and file-kind guards of #2057/#2079, the write path, and the client are untouched.

core/test/workflow-store.test.js — three red-first cases on a temporary workspace with a definition written outside workflows/: open/bytesOf/customize answer not-found, the workflows endpoint's preview and open answer not-found, and a stored default naming such an id is answered as null. Each case keeps a control in the same test (a legal workspace id and the blank preset still open), so the guard cannot switch the feature off. Without the source change these three fail (ok: true, SECRET-OUTSIDE-WORKFLOWS came back); with it the file is 23/23 green.

docs/subsystems/core-host.md — the workflow-store.js row now says the read path accepts only isWorkflowId ids, that ../outside/stolen answers workflow/not-found, and that no file outside workflows/ is read.

changelog.d/2609.fixed.md — the user-visible line.

Ran at this tip:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/workflow-store.test.js
exit 0
1..23  # tests 23  # pass 23  # fail 0  # skipped 0  # todo 0

$ cd core && dev/harness/node_modules/.bin/oxlint src/host/workflow-store.js test/workflow-store.test.js
Found 0 warnings and 0 errors.

$ node scripts/ds-bot-patches.mjs check
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/

$ node core/scripts/build-client.mjs --check
exit 0   (no output; core/client.js is untouched)

$ node .github/scripts/changelog.mjs check
changelog: 14 fragment(s) in changelog.d/ are well formed

$ node .github/scripts/check-mojibake.mjs --check
mojibake: ok — 0 occurrences of U+FFFD in 1538 text file(s)

$ cd plugins/workflow && node --test test/temp-name.test.js
# tests 2  # pass 2  # fail 0

Red before the source change, same file: 3 failed (ok: true for ../outside/stolen; list().default echoed the id) — the case's evidence file records the commands and output.

Not run, and why: npm run verify (the task scopes verification to the target file plus the generated-file checks on this shared box); node .github/scripts/check-arch-docs.mjs (cannot diff against origin/main — no git here; the owned page is updated in this diff anyway); node scripts/ds-bot-patches.mjs generate/canonical (both need git). For the generated side, the two changed core files are whole-file mirrors upstream does not have, so patches/ds-bot/files/ was updated with byte-identical copies of them — which is what generate writes — and check is green. The smoke test was not run: no host wiring, tool or delivery path changed.

Checkpoint

{
  "pr": 2610,
  "claim": "Dev",
  "issue": 2609,
  "summary": "The workflows read path joined an unvalidated id onto workflows/, so an id like ../outside/stolen read a JSON file written outside the workspace's workflows folder and served it as this workspace's own definition. readDefinition now refuses an id that is not isWorkflowId, which covers open, preview, bytesOf, customize, setDefault and the stored default key list() reads back. An illegal id answers the existing workflow/not-found; legal and preset ids are unchanged.",
  "files": ["core/src/host/workflow-store.js", "core/test/workflow-store.test.js", "docs/subsystems/core-host.md", "changelog.d/2609.fixed.md", "patches/ds-bot/files/src/host/workflow-store.js", "patches/ds-bot/files/test/workflow-store.test.js"],
  "checks": { "ran": "cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/workflow-store.test.js", "result": "green", "at": "6a04d47dcf2f39bb691aad9d823c367bc281f4d0" },
  "next": "none — ready for review"
}

The read path joined an unvalidated id onto workflows/, so an id such as
../outside/stolen read a JSON definition written outside the workspace's
workflows/ folder. readDefinition now accepts only the ids isWorkflowId
allows, the same rule save already used, so an illegal id answers the
existing workflow/not-found and legal and preset ids are unchanged.

Refs #2609
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 7, 2026
@Birfy Birfy self-assigned this Nov 7, 2026
@Birfy

Birfy commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 6a04d47
Written-by: Reviewer
Attestation: 6a04d47 role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/workflow-store.test.js
# tests 23  # pass 23  # fail 0
$ cp base-workflow-store.js tip/core/src/host/workflow-store.js && cd tip/core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/workflow-store.test.js
# tests 23  # pass 20  # fail 3   (base blob, only the three new #2609 cases red)
$ node scripts/ds-bot-patches.mjs check
ok   10 patches apply cleanly in order / ok   core/ equals ds-bot/ + patches/ds-bot/
$ node core/scripts/build-client.mjs --check
exit 0
$ node .github/scripts/changelog.mjs check
changelog: 14 fragment(s) in changelog.d/ are well formed

Review — #2609 at 6a04d47

  • Red first. Tip tarball at 6a04d47d, core/node_modules linked from the shared harness: 23/23 green. Replacing only core/src/host/workflow-store.js with the base blob (0d922cc8) turns exactly the three new cases red (20 pass / 3 fail: open/bytesOf/customize, the workflows endpoint preview/open, the stored default) and nothing else. Restored to the tip blob afterwards, byte-identical.
  • The guard is in the right place. readDefinition (workflow-store.js:100-101) refuses a non-isWorkflowId id before any join; every read entry point reaches it — open (:139), customize (:196), setDefault (:205), the endpoint (:229) and list()'s stored default (:134) — so nothing outside workflows/ is read and legal/preset ids are unchanged (each test keeps that control).
  • Scope is the six files claimed. core/src/host/workflow-store.js, core/test/workflow-store.test.js, docs/subsystems/core-host.md (owned page updated), changelog.d/2609.fixed.md, and the two patches/ds-bot/files/ mirrors, which are byte-identical to the core files (cmp). No core/client.js, no ruleset, no CI change; ds-bot-patches check and build-client --check green.
  • Tip state. Base is live main 0d922cc8. tests-ok, package, all three plugin suites and merge-gate green at the tip; enqueue-guard red only with no Verifier report names the tip, which this comment answers. No hold, no do-not-merge comment, agent-pr present, body Fixes #2609 with Owner: Dev / Reviewer: Reviewer, checkpoint block well formed (1024 chars, 6 files, at = this tip).
  • Not run: full npm run verify and the smoke test — no host wiring/tool/delivery path changed; the target suite plus the generated-file checks cover this diff.

Admission line: a reproducible path-escape defect with a test that fails on the base source, scoped to one host function.

@github-actions github-actions Bot added the pass label Nov 7, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit 80151ce Nov 7, 2026
17 of 19 checks passed
@Birfy
Birfy deleted the dev/2609-workflow-store-id-guard branch November 7, 2026 13:28
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(host): workflows read joins an unvalidated id, so ../ opens a JSON file outside the workflows folder

1 participant